BackTrader 三均线策略
介绍
三均线与《BackTrader SMA 金叉策略》类似,区别在于采用 3 条均线(短期、中期、长期)来作为决策点,将长期趋势也考虑在内。
本文策略是在《Backtrader来啦:常见案例汇总》一文中提出的,我进行了复现。
本文只用于数学、编程研究,不提供交易指导。
策略介绍
选取5日短期均线、20日中期均线,60日长期均线。
买入条件:当前无持仓,5日 > 20日 > 60日(多头排列),次日买入开仓。
卖出条件:当前有持仓,5日下穿20日,次日卖出清仓。
代码实现
代码实现也参照《Backtrader来啦:常见案例汇总》,从中学到一招,原来不用创建策略类,只靠自定义指标就能够直接参与交易。不过我还是把自定义指标加入我的策略类中来跑。
代码实现:
import backtrader as bt
from newstock.data.mongo.mongo_data_manager import MongoDataManager
from newstock.date.stock_date import StockDate
from newstock.market.Exchange import SZSEExchange
from newstock.market.symbol import Symbol
import pandas as pd
class MySignal(bt.Indicator):
lines = ("signal",) # 声明 signal 线,交易信号放在 signal line 上
params = dict(short_period=5, median_period=20, long_period=60)
def __init__(self):
self.s_ma = bt.ind.SMA(period=self.p.short_period)
self.m_ma = bt.ind.SMA(period=self.p.median_period)
self.l_ma = bt.ind.SMA(period=self.p.long_period)
# 短期均线在中期均线上方,且中期均取也在长期均线上方,三线多头排列,取值为1;反之,取值为0
self.signal1 = bt.And(self.m_ma > self.l_ma, self.s_ma > self.m_ma)
# 求上面 self.signal1 的环比增量,可以判断得到第一次同时满足上述条件的时间,第一次满足条件为1,其余条件为0
self.buy_signal = bt.If((self.signal1 - self.signal1(-1)) > 0, 1, 0)
# 短期均线下穿长期均线时,取值为1;反之取值为0
self.sell_signal = bt.ind.CrossDown(self.s_ma, self.m_ma)
# 将买卖信号合并成一个信号
self.lines.signal = bt.Sum(self.buy_signal, self.sell_signal * (-1))
class TestStrategy(bt.Strategy):
params = (("printlog", False),)
def log(self, txt, dt=None, doprint=False):
"""Logging function for this strategy"""
if self.params.printlog or doprint:
dt = dt or self.datas[0].datetime.date(0)
print("%s, %s" % (dt.isoformat(), txt))
def __init__(self):
# Keep a reference to the "close" line in the data[0] dataseries
self.dataclose = self.datas[0].close
# To keep track of pending orders
self.order = None
self.buyprice = None
self.buycomm = None
self.signal = MySignal(self.datas[0])
self.s_ma = bt.ind.SMA(period=5)
self.m_ma = bt.ind.SMA(period=20)
self.l_ma = bt.ind.SMA(period=60)
bt.indicators.MACDHisto(self.datas[0])
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
return
# 交易完成
# Attention: broker could reject order if not enough cash
if order.status in [order.Completed]:
if order.isbuy():
self.log(
"BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f"
% (order.executed.price, order.executed.value, order.executed.comm)
)
self.buyprice = order.executed.price # 买入价格
self.buycomm = order.executed.comm # 买入手续费
elif order.issell():
self.log(
"SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f"
% (order.executed.price, order.executed.value, order.executed.comm)
)
self.bar_executed = len(self) # 买入日期
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log("Order Canceled/Margin/Rejected")
# Write down: no pending order
self.order = None
def notify_trade(self, trade):
if not trade.isclosed:
return
self.log("OPERATION PROFIT, GROSS %.2f, NET %.2f" % (trade.pnl, trade.pnlcomm))
def next(self):
# Simply log the closing price of the series from the reference
self.log("Close, %.2f" % self.dataclose[0])
# Check if an order is pending ... if yes, we cannot send a 2nd one
if self.order:
return
# Check if we are in the market
if not self.position:
# Not yet ... we MIGHT BUY if ...
if self.signal == 1:
# current close less than previous close
# BUY, BUY, BUY!!! (with default parameters)
self.log("BUY CREATE, %.2f" % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.buy()
else:
# Already in the market ... we might sell
if self.signal == -1:
# SELL, SELL, SELL!!! (with all possible default parameters)
self.log("SELL CREATE, %.2f" % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.sell()
def stop(self):
self.log(
"Ending Value %.2f" % (self.broker.getvalue()),
doprint=True,
)
if __name__ == "__main__":
cerebro = bt.Cerebro()
print("Starting Portfolio Value: %.2f" % cerebro.broker.getvalue())
mongoManager = MongoDataManager()
df = mongoManager.getStockPeriodFromDB(
Symbol(SZSEExchange, "000001"),
StockDate.today().previousDays(500),
StockDate.today(),
)
df["date"] = pd.to_datetime(df["trade_date"], format="%Y%m%d")
data = bt.feeds.PandasData(dataname=df, datetime="date") # type: ignore
df.dropna()
# 0.1% ... divide by 100 to remove the %
cerebro.broker.setcommission(commission=0.001)
# Python 3.10 修复 module 'collections' has no attribute 'Iterable' 开始
import collections
collections.Iterable = collections.abc.Iterable
# Python 3.10 修复 module 'collections' has no attribute 'Iterable' 完成
# 策略参数优化
# cerebro.optstrategy(TestStrategy, maperiod=range(10, 31))
# 策略运行
cerebro.addstrategy(TestStrategy)
cerebro.adddata(data)
# Add a FixedSize sizer according to the stake
cerebro.addsizer(bt.sizers.FixedSize, stake=10)
cerebro.run()
cerebro.plot(style="bar", volume=False)
print("Final Portfolio Value: %.2f" % cerebro.broker.getvalue())
执行效果
原参数效果
由于我本地数据量并不够,因此没能完成一次有效交易。使用 zh000001 最近的交易日,效果如下图:
我发现,5日下穿20日作为一个卖出信号还是不错的。
缩短周期
由于本地数据量不够,考虑缩短周期,用三均线作为短期策略使用,看看效果。
3条均线分别改为 5,15,20:
我发现,5,15,20 的多头排列作为开仓信号还不错。
两者结合
既然:
- 5日下穿20日作为一个卖出信号还是不错的。
- 5,15,20 的多头排列作为开仓信号还不错。
两者结合一下会怎样呢?
效果跟想的一样,两个大的上涨波段都捕捉到了(第一个因为数据量不足,没有开仓成功)。
中间还试图捕捉一个小的趋势,但是失败了,并且损失还挺大。